文章目录
  1. 1. 第30条:用枚举代替int常量
  2. 2. 第31条:用实例域代替序数
  3. 3. 第32条:用EnumSet代替位域
  4. 4. 第33条:用EnumMap代替序数索引
  5. 5. 第34条:用接口模拟可伸缩的枚举
  6. 6. 第35条:注解优于命名模式
  7. 7. 第36条:坚持使用Override注解
  8. 8. 第37条:用标记接口定义类型

第30条:用枚举代替int常量

在编程语言中还没有引入枚举类型之前,表示枚举类型的常用模式就是声明一组具名的int常量

1
2
3
public static final int APPLE_FUJI			=0;
public static final int APPLE_PIPPIN =1;
public static final int APPLE_GRANNY_SMITH =2;

但是它又诸多的缺点:

  • 在类型安全性和使用方便性方面没有任何帮助。
  • 如果关联的int放生了变化,客户端必须得重新编译。
  • 打印调试的时候只能打印数字

上述int常量如何使用枚举类实现

1
public enum APPLE{FUJI,PIPPIN,GRANNY_SMITH};

除了编码简单之外,枚举的特点还有:

  • 枚举是类型安全的(所有可以作为单例的泛型化来使用)。
  • 可以增加或者重新排列枚举类型中的常量,而无需重新编译它的客户端代码。
  • 可以通过toString方法来打印可视的字符串。

枚举类型还可以添加任意的方法和域,并实现任意的接口。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
interface Calc{
double apply(double x,double y);
}

/**
* 具体继承了接口 并重新实现了
* @author yanyl
*
*/

public enum Operation implements Calc{
PLUS("+"){
public double apply(double x,double y){return x+y;}
},
MINUS("-"){
public double apply(double x,double y){return x-y;}
},
TIMES("*"){
public double apply(double x,double y){return x*y;}
},
DIVIDE("/"){
@Override
public double apply(double x,double y){return x/y;}
};
private final String symbol;
Operation(String symbol)
{
this.symbol=symbol;
}

@Override
public String toString()
{

return this.symbol;
}
}

上述枚举可以这么使用

1
2
Calc calc=Operation.PLUS;
System.out.println(calc.apply(1, 2));

是不是超级方便~

总之而言,与int常量对比,枚举类型的优势是不言而喻的,(当然,当初枚举的出现就是为了解决int常量问题的^_^,所以大家还是多使用枚举吧)

第31条:用实例域代替序数

什么是枚举的序数,你知道吗?

1
2
3
4
5
6
7
8
9
public enum Ensemble{
SOLO,DUET,TRIO,QUARTET,QUITTET,
SEXTET,SEPTET,OCTET,NONET,DECTET;

public int numberOfMusicians()
{

return ordinal()+1;
}
}

每个枚举都有一个ordinal()方法,用于返回当前枚举值的序数,这个见都没见过的方法用起来看似很方便,但是如果你要修改常量的顺序,那么维护起来就是一场噩梦。
第30条说过了,枚举里面可以有字段,所以这条就是推荐使用实例域代替序数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public enum Ensemble{
SOLO(1),DUET(2),TRIO(3),QUARTET(4),QUITTET(5),
SEXTET(6),SEPTET(7),OCTET(8),NONET(9),DECTET(10);

/**
* 使用自定义的一个实例域
*/

private final int numberOfMusicians;
Ensemble(int numberOfMusicians)
{
this.numberOfMusicians=numberOfMusicians;
}

public int numberOfMusicians()
{

return numberOfMusicians+1;
}
}

这个方法实现的修过虽好,然是感觉写起来好麻烦。。。维护起来会很简单吗???

第32条:用EnumSet代替位域

这个小节讲的其实就是和位图法相关。

如果一个枚举类型的元素主要用于在集合中,一般就使用int枚举类型,将2的不同倍数赋予每个常量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Text
{

public static final int STYLE_BOLD =1<<0;//1
public static final int STYLE_ITALIC =1<<1;//2
public static final int STYLE_UNDERLINE =1<<2;//4
public static final int STYLE_STRIKETHROUGH =1<<3;//8


private int styles=-1;
public void applyStyles(int styles)
{

this.styles=styles;//这里其实就是用位图法来保存,取值要用位移来取 int这里只能存32个值
}
}

这种表示方法让你用OR位运算将几个常量合并到一个集合中,称作位域。
然后你可能会使用这或操作种方法来保持状态:

1
new Text().applyStyles(Text.STYLE_BOLD|Text.STYLE_ITALIC);

这种做法一个是打印是来比较难看懂,还有编译表示所有元素时也没好好的办法。

还好有EnumSet的出现,它实现了Set接口,如果底层枚举类型少于64个,那么这个EnumSet就是用一个long类型来表示,所以性能上也是很高的,上面这个例子用EnumSet来实现就是:

1
2
3
4
5
6
7
8
9
10
static class Text
{

public enum Style{BOLD,ITALIC,UNDERLINE,STRIKETHROUGH}

private EnumSet<Style> styles=null;
public void applyStyles(EnumSet<Style> styles)
{

this.styles=styles;
}
}

调用方法就用

1
new Text().applyStyles(EnumSet.of(Text.Style.BOLD, Text.Style.ITALIC));

这是调用之后你打印styles这个变量可以看到

[BOLD, ITALIC]

这种打印显示的总比单纯的数字要美好的多。

总而言之,正式因为枚举类型要用集合,所以没有理由用位域来表示它。
其实我觉得用EnumSet应该没有位域快啊,估计高手还是会去用位域的把

第33条:用EnumMap代替序数索引

1
2
3
public class Herb{
public enum Type {ANNUL,PERENNIAL,BIENNIAL}
}

当你需要将上面类的实例根据Type枚举类型来存储起来是,它推荐这么做:

1
Map<Herb.Type,Set<Herb>> enumMap=new EnumMap<Herb.Type,Set<Herb>>(Herb.Type.class);

但是我觉得何必这么麻烦,用下面的写得表示也很方便嘛,也还不需要记那么多东西

1
Map<Integer,Set<Herb>> map=new HashMap<Integer,Set<Herb>>();

每次使用Type.ordinal()的索引即可。

第34条:用接口模拟可伸缩的枚举

前面几条讲了使用Enum的便利性,安全性,但是它的可伸缩性比较弱,因为枚举无法再继承类或者枚举,比较幸运地时枚举支持接口的实现。
例如第30条中的四则运算我需要扩展其他的运算:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public enum ExtendOperation implements Calc{
EXP("^"){
public double apply(double x,double y){return Math.pow(x,y);}
},
REMAINDER("%"){
public double apply(double x,double y){return x%y;}
};

private final String symbol;
ExtendOperation(String symbol)
{
this.symbol=symbol;
}

@Override
public String toString()
{

return this.symbol;
}
}

然后你就可以这么使用

1
2
3
4
5
6
7
8
9
10
public static void main(String[] args)
{

System.out.println(test(Operation.PLUS,2,3));//调用加法
System.out.println(test(ExtendOperation.EXP,2,3));//调用求幂
}

public static <T extends Enum<T> & Calc> double test(T opt,double x,double y)
{

return opt.apply(x, y);
}

最终会输出

5.0
8.0

总而言之,虽然无法编写可扩展的枚举类型,却可以通过编写接口以及实现该接口的基础枚举类型,对它进行模拟。

第35条:注解优于命名模式

JUnit为例,命名模式的3大缺陷:

  1. 文字拼写错误会导致失败,并且没有任何提示
  2. 无法确保他们只用于相应的程序元素上
  3. 它们都没有提供将参数值与程序元素关联起来的好方法

你这通过这种方式简单的来创建一个自己的注解

1
2
3
4
5
6
7
import java.lang.annotation.*;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface MyTest {

}

MyTest注解类型的声明就是它自身的RetentionTarger注解进行了注解,这些注解类型称为元注解,其中:

  • @Retention(RetentionPolicy.RUNTIME)表示MyTest注解应该在运行时保留
  • @Target(ElementType.METHOD)表示该注解仅用于方法上

我们有了这个注解之后可以这么使用它

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class HelloWorld{

@MyTest
public void test1(){}

public void test2(){}

@MyTest
public void test3(){}

@MyTest
public void test4() throws Exception
{

throw new Exception("i am error test4");
}
}

那我们如何调用这个注解呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import java.lang.reflect.*;

public class RunTests {
public static void main(String[] args) throws Exception
{

int tests=0;
int passed=0;
Class testClass=Class.forName("yyl.java.study.test.HelloWorld");
Object testClassInstance=testClass.newInstance();
for(Method m:testClass.getDeclaredMethods())
{
if(m.isAnnotationPresent(MyTest.class))
{
tests++;
try
{
m.invoke(testClassInstance,null);//对实例的方法进行调用
passed++;
System.out.println("PASSED:"+m.getName());
}catch(Exception e)
{
System.out.println("FAILD:"+m.getName());
}

}
}

System.out.println(String.format("PASSED:%s,FAILED:%s",passed,tests-passed));

}
}

启动这个RunTests会得到

PASSED:test3
FAILD:test4
PASSED:test1
PASSED:2,FAILED:1

一个简易版的测试框架就完成了,该书中的其他内容是讲你如何完善这个自制的测试框架,比如添加异常的注解,最终判断失败的机制等,我个人觉得只要把上述的简易框架看懂其他都不是问题。^_^

既然有了注解,那么完全没有理由再使用命名模式了。

第36条:坚持使用Override注解

估计Override注解是在Java中最常见也是常用的一种注解了吧,它标志方法被重写,但是在java中你重写方法时不加Override也是可以正常运行的,所以我相信很多人会懒掉这个注解,关于这个注解书中有下面几个推荐理由:

  • 你再重写得定义上出错时如果有Override,编译器就会帮你检查错误
  • IDE具有自动检查功能,当你没有用Override但是却覆盖了超类的方法时,IDE就是产生一条警告,提醒确认。
  • 还可以让程序员清晰地看到这条方式是否是重写方法,在看代码的找起来比较方便啊(我自己觉得的)

总而言之,如果在你想要的每个方法声明中使用Override注解来覆盖超类声明,编译器就可以替你防止大量的错误(在继承抽象类时可以不加。。但是我觉得还是加的好)

第37条:用标记接口定义类型

标记接口是没有包含方法声明的接口,而只是指明一个类实现了具有某种属性的接口,比如Serializable

关于标记接口与标记注解的争论:

标记接口的优势:

  • 标记接口定义的类型是由被标记类的实例实现的;标记注解则没有定义这样的类型
  • 标记接口可以更加精确地进行锁定

标记注解的优势:

  • 它可以通过默认的方式添加一个或者多个注解类型的元素
  • 标记注解作为变成元素之一的框架中同样具有一致性。

总而言之,标记接口和标记注解都各有用处,如果想要定义一个任何新的方法都不会与之关联的类型,标记接口是最好的选择。

文章目录
  1. 1. 第30条:用枚举代替int常量
  2. 2. 第31条:用实例域代替序数
  3. 3. 第32条:用EnumSet代替位域
  4. 4. 第33条:用EnumMap代替序数索引
  5. 5. 第34条:用接口模拟可伸缩的枚举
  6. 6. 第35条:注解优于命名模式
  7. 7. 第36条:坚持使用Override注解
  8. 8. 第37条:用标记接口定义类型